package com.phil.philreports.asynccelltablesearch.server.service;

import java.io.Serializable;
import java.util.Collections;
import java.util.TreeMap;

import net.sf.jsr107cache.Cache;
import net.sf.jsr107cache.CacheException;
import net.sf.jsr107cache.CacheManager;

/**
 * 
 * @author Philipp Seeger
 *
 * This class helps finding a cursor for a given query
 * in the memcache and stores new cursors in the memcache.
 * In a running environment this class drastically lowers
 * query cost due to smaller offsets while fetching datasets.
 */
public class CursorCacher implements Serializable {

	private static final long serialVersionUID = -467638050552525796L;

	/**
	 * The minimal distance in rows between two cursors.
	 * If a new cursor is just one or two indexes next to
	 * one already in the cache, we might not want to add
	 * another one.
	 */
	private int minCursorDistance = 12;
	
	/**
	 * A identifier for a given query.
	 * This class is not responsible for creating or
	 * managing those identifiers.
	 * It's recommended to format it like
	 * [EntityClassName]+[filterarguments]
	 */
	private String uniqueQueryIdentifier;
	
	/**
	 * Cursors for a given query are stored here.
	 * The TreeMap searches for indexes with a cost
	 * of log(number of indexes).
	 */
	private TreeMap<Integer, String> cursors;
	
	/**
	 * The cache
	 */
	private Cache cache;
	
	/**
	 * This class tries to find cursors left to
	 * or equal to this index for a given query
	 */
	private int startIndex;
	
	/**
	 * The default constructor used by implementations that
	 * might extends this class.
	 */
	public CursorCacher() {
		this.uniqueQueryIdentifier = "";
		this.cursors = null;
		this.cache = null;
		this.startIndex = 0;
	}
	
	/**
	 * Constructor
	 * 
	 * It's recommended to use this constructor to not
	 * forget the necessary attribute to identify the query.
	 * 
	 * @param uniqueQueryIdentifier	String identifier of the query
	 */
	public CursorCacher(String uniqueQueryIdentifier) {
		this.uniqueQueryIdentifier = uniqueQueryIdentifier;
		this.startIndex = 0;
		
		this.cursors = null;
		this.cache = null;
	}
	
	/**
	 * Initializes the cache. Before finding or storing
	 * cursors this method has to be used to connect
	 * to the memcache.
	 * 
	 * @throws CacheException
	 */
	public void initCache() throws CacheException {
		this.cache = CacheManager.getInstance().getCacheFactory().createCache(Collections.emptyMap());
	}
	
	/**
	 * This method calls the CachedCursor(int startIndex) with the
	 * current startIndex attribute.
	 * 
	 * @return	CatchedCursor	The representation of a Cursor with an index
	 * 							and String cursor attribute
	 * @throws Exception
	 */
	public CachedCursor findCursor() throws Exception {
		return findCursor(this.startIndex);
	}
	
	/**
	 * Returns a CachedCursor cursor with an index smaller than or equal
	 * than the given parameter from the memcache.
	 * 
	 * If this class has no unique query identifier or the cache isn't
	 * initialized it throws an Exception.
	 * 
	 * If there is no cursor in the cache it returns null.
	 * 
	 * @param startIndex
	 * @return CachedCursor
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	public CachedCursor findCursor(int startIndex) throws Exception {
		this.setStartIndex(startIndex);
		// if the cache isnt initialized
		// throw exception
		if(this.cache == null) {
			throw new Exception("Cache not initialize. Use initCache() before trying to find a cursor.");
		} 
		// if there is no unique query identifier
		// this method wouldnt know what to look for
		// throws Exception
		if(uniqueQueryIdentifier.isEmpty()) {
			throw new Exception("No unique query identifier found. You have to set a unique query identifier first.");
		}
		try {
			
			// try to find the map holding cursors for the
			// given query id from the cache
			cursors = (TreeMap<Integer, String>) cache.get(this.uniqueQueryIdentifier);
			
			// no cursors for this query found
			if(cursors == null) {
				return null;
			}
			
			// at this point we know that there are
			// cursors for the given query
			// lets find the closest one with a index
			// smaller than or equal to the startIndex parameter
			Integer cachedCursorIndex = cursors.floorKey(startIndex);
			
			// if there is no index smaller than or equal
			// there is no useful cursor
			// return null
			if(cachedCursorIndex == null) {
				return null;
			}
			// get the String representation of the cursor in the cached
			// curser map
			String cachedCursorString = cursors.get(cachedCursorIndex);
			
			// Create a CachedCursor objects which hold the index of the
			// cursor (is smaller than or equal to the startIndex parameter)
			// and the String representation of the cursor
			CachedCursor cachedCursor = new CachedCursor(cachedCursorIndex, cachedCursorString);
			
			// return the CachedCursor
			return cachedCursor;
			
		} catch (ClassCastException cce) {
			// couldn't cast the cached entry with the key
			// equal to the unique query identifier
			// to TreeMap<Integer, String>
			// another problem must have inserted corrupted data
			throw new Exception("The cached object with index '" + this.uniqueQueryIdentifier + "' is not a TreeMap<Integer, String>");
		}
	}
	
	/**
	 * Tries to store a given CachedCursor in the cache.
	 * <p>
	 * This method uses the min cursor distance to determine
	 * if there is enough space between two given cursors
	 * at the CachedCursor's index position. If not it returns
	 * false. If there are no cursors for the unique query id
	 * in the cache yet, it creates a new list of cursors for
	 * this specific query id in the cache.
	 * </p>
	 * @throws	Exception	If there is no initialized cache or unique query identifier
	 * 						or corrupted data in the cache
	 * 
	 * @param	CachedCursor	a CachedCursor
	 * 
	 * @return	true	if the cursor has been successfully stored in the cache
	 * 					or false if there was a problem
	 * 
	 * @see		boolean
	 */
	@SuppressWarnings("unchecked")
	public boolean cacheCursor(CachedCursor cursor) throws Exception {
		
		// if the cursors index is smaller than the min distance between
		// cursors, it means that the min distance allowed is larger than
		// the first element overall for this query.
		// No cursor necessary there.
		if(cursor.getIndex() < this.minCursorDistance) {
			return false;
		}
		
		// if the cache hasn't been initialized throw
		// an exception
		if(this.cache == null) {
			throw new Exception("Cache not initialize. Use initCache() before trying to find a cursor.");
		} 
		
		// if the uniqueQueryIdentifier is empty there is
		// an exception thrown
		if(uniqueQueryIdentifier.isEmpty()) {
			throw new Exception("No unique query identifier found. You have to set a unique query identifier first.");
		}
		
		// Find the next cursor smaller and bigger than the given cursors index
		// so it is possible to check if there is enough room to the left and right
		// for the new cursor to fit in.
		try {
			
			// trying to get a map of cursors for the given query
			// if there is no map, i continue after the if statement
			// and expect that to be the first cursor stored for this
			// kind of query
			cursors = (TreeMap<Integer, String>) cache.get(this.uniqueQueryIdentifier);
			
			// cursors is null if there is no TreeMap<Integer, String>
			// in the cache for the current unique query id
			if (cursors != null) {
				
				// in cursors we do now have a TreeMap of cursors where the
				// index is an Integer of the row for the query.
				// Let's see if there are smaller or bigger ones
				// already stored.
				Integer smallerThanOrEqual = cursors.floorKey(cursor.getIndex());
				Integer largerThanOrEqual = cursors.ceilingKey(cursor.getIndex());
				
				// if there is no smaller one
				// we set the smallerThanOrEqual to 0
				// as if it represented a cursor to the first
				// entity (which is the same as if you wouldn't use a cursor)
				if(smallerThanOrEqual == null) {
					smallerThanOrEqual = 0;
				}
				
				// if there is no larger index
				// set largerThanOrEqual to a number larger than
				// the current cursors index + the minimal distance
				if(largerThanOrEqual == null) {
					largerThanOrEqual = cursor.getIndex()+this.minCursorDistance;
				}
				
				// Check the distance
				int distanceToSmaller = cursor.getIndex()-smallerThanOrEqual;
				
				// if the distance to a smaller index is smaller
				// than the minimum distance we dont have to store
				// a new cursor
				if(distanceToSmaller < this.minCursorDistance) {
					return false;
				}
				
				int distanceToLarger = largerThanOrEqual-cursor.getIndex();
				
				// if the distance to the next larger index is smaller
				// than the minimum distance, we don't have to store
				// a new cursor
				if(distanceToLarger < this.minCursorDistance) {
					return false;
				}
				
			} else {
				// this cursor is the first one for the given query
				
				// if it's index is smaller than the minimum distance
				// to the index of the first entity (index 0)
				// we don't need to insert a new cursor
				if(cursor.getIndex() < this.minCursorDistance) {
					return false;
				}
				// it was the first cursor for the given query
				// let's make a new map which later gets stored in the
				// memcache
				cursors = new TreeMap<Integer, String>();
			}
			
			// store the new cursor in the map holding
			// the cursors for the given query id
			cursors.put(cursor.getIndex(), cursor.getCursor());
			
			// and insert that map back into the memcache
			// under the index of the query identifier
			cache.put(this.uniqueQueryIdentifier, cursors);
			
			// return true because a new cursor was stored
			// in the memcache
			return true;
		
		} catch (ClassCastException cce) {
			// there has been a cache entry with the index
			// of a unique query id found, but it was not a
			// map of cursors.
			throw new Exception("The cached object with index '" + this.uniqueQueryIdentifier + "' is not a TreeMap<Integer, String>");
		}
	}
	
	/**
	 * Sets the minimal distance from one cursor index to
	 * another. When inserting new cursors it prevents
	 * inserting too many too close together.
	 * 
	 * @param minimalCursorDistance int
	 */
	public void setMinCursorDistance(int i) {
		if(i > 0) {
			this.minCursorDistance = i;
		}
	}
	
	/**
	 * returns the current minimal allowed cursor distance
	 * to already stored cursors by indexes.
	 * 
	 * @return int minimalCursorDistance
	 */
	public int getMinCursorDistance() {
		return this.minCursorDistance;
	}
	
	/**
	 * Sets a String unique identifier which identifies
	 * a query.
	 * This cant be an empty String after String.trim() or null
	 * 
	 * @param id
	 */
	public void setUniqueQueryIdentifier(String id) {
		if(id != null && !id.trim().isEmpty()) {
			this.uniqueQueryIdentifier = id;
		}
	}
	
	/**
	 * Returns the current unique query identifier
	 * 
	 * @return String
	 */
	public String getUniqueQueryIdentifier() {
		return this.uniqueQueryIdentifier;
	}
	
	/**
	 * Sets the start Index for cursors that should be found
	 * This has to be larger than 0
	 * 
	 * @param startIndex
	 */
	public void setStartIndex(int startIndex) {
		if(startIndex > 0) {
			this.startIndex = startIndex;
		}
	}
	
	/**
	 * Returns the current start Index
	 * 
	 * @return int
	 */
	public int getStartIndex() {
		return this.startIndex;
	}
	
}
